package repository

import (
	"basic-go/webook/internal/domain"
	"basic-go/webook/internal/repository/cache"
	"basic-go/webook/internal/repository/dao"
	"database/sql"
	"log"

	"golang.org/x/net/context"
)

// 通过别名的方式定义repository层的错误,用于给上层(service层)判定错误的类型
var (
	ErrDuplicateUser = dao.ErrDuplicateEmail
	// IMPT: repository层和业务是强相关的,所以定义的UserNotFound,而不是RecordNotFound
	ErrUserNotFound = dao.ErrRecordNotFound
)

type UserRepository interface {
	Create(ctx context.Context, u domain.User) error
	FindByEmail(ctx context.Context, email string) (domain.User, error)
	FindById(ctx context.Context, uid int64) (domain.User, error)
	FindByPhone(ctx context.Context, phone string) (domain.User, error)
}

// 支持缓存的UserRepository实现
type CachedUserRepository struct {
	dao   dao.UserDAO
	cache cache.UserCache
}

func NewCachedUserRepository(dao dao.UserDAO, c cache.UserCache) UserRepository {
	return &CachedUserRepository{
		dao:   dao,
		cache: c,
	}
}

func (repo *CachedUserRepository) Create(ctx context.Context, u domain.User) error {
	return repo.dao.Insert(ctx, repo.toEntity(u))
}

func (repo *CachedUserRepository) FindByEmail(ctx context.Context, email string) (domain.User, error) {
	u, err := repo.dao.FindByEmail(ctx, email)
	if err != nil {
		return domain.User{}, err
	}
	return repo.toDomain(u), nil
}

func (repo *CachedUserRepository) toDomain(u dao.User) domain.User {
	return domain.User{
		Id:       u.Id,
		Email:    u.Email.String,
		Password: u.Password,
		Phone:    u.Phone.String,
	}
}

func (repo *CachedUserRepository) toEntity(u domain.User) dao.User {
	return dao.User{
		Id: u.Id,
		Email: sql.NullString{
			String: u.Email,
			Valid:  u.Email != "",
		},
		Phone: sql.NullString{
			String: u.Phone,
			Valid:  u.Phone != "",
		},
		Password: u.Password,
	}
}

func (repo *CachedUserRepository) FindById(ctx context.Context, uid int64) (domain.User, error) {
	// 先从缓存中进行查询
	du, err := repo.cache.Get(ctx, uid)
	if err == nil {
		return du, nil
	}
	// 这里的err有两种可能
	// 1. key不存在，说明redis是正常的
	// 2. 访问redis有问题，可能是网络的问题，也可能是redis本身就崩溃了

	// 没有命中，则直接从数据库进行查询
	u, err := repo.dao.FindById(ctx, uid)
	if err != nil {
		return domain.User{}, err
	}
	du = repo.toDomain(u)

	// 尝试将数据写入缓存

	// 异步写入缓存的方式
	// go语言中建议这么做，因为协程非常轻量，使用也方便
	// go func() {
	// 	err = repo.cache.Set(ctx, du)
	// 	if err != nil {
	// 		// 网络崩了，也可能是redis崩了
	// 		// 会造成缓存击穿，因为每次都会访问数据库
	// 		log.Println(err)
	// 	}
	// }()

	// 同步写入缓存的方式
	err = repo.cache.Set(ctx, du)
	if err != nil {
		log.Println(err)
	}

	return du, nil
}

func (repo *CachedUserRepository) FindByIdV1(ctx context.Context, uid int64) (domain.User, error) {
	// 先从缓存中进行查询
	du, err := repo.cache.Get(ctx, uid)
	switch err {
	case nil:
		return du, nil
	case cache.ErrKeyNotExist:
		// key不存在的情况，则直接从数据库进行查询
		u, err := repo.dao.FindById(ctx, uid)
		if err != nil {
			return domain.User{}, err
		}
		du = repo.toDomain(u)

		// 尝试将数据写入缓存

		// 异步写入缓存的方式
		// go语言中建议这么做，因为协程非常轻量，使用也方便
		go func() {
			err = repo.cache.Set(ctx, du)
			if err != nil {
				// 网络崩了，也可能是redis崩了
				// 会造成缓存击穿，因为每次都会访问数据库
				log.Println(err)
			}
		}()

		// 同步写入缓存的方式
		// err = repo.cache.Set(ctx, du)
		// if err != nil {
		// 	log.Println(err)
		// }

		return du, nil
	default:
		// 其他错误，如网络或者redis异常
		// 这是一种接近降级的写法
		// 为了保护系统，避免缓存雪崩
		// redis有异常则不查询数据库
		return domain.User{}, err
	}
}

func (repo *CachedUserRepository) FindByPhone(ctx context.Context, phone string) (domain.User, error) {
	u, err := repo.dao.FindByPhone(ctx, phone)
	if err != nil {
		return domain.User{}, err
	}
	return repo.toDomain(u), nil
}
